package cn.tedu.charging.cost.service.impl;


import cn.tedu.charging.common.pojo.param.ProgressCostParam;
import cn.tedu.charging.common.pojo.po.ChargingCostRulePO;
import cn.tedu.charging.common.pojo.vo.ProgressCostVO;
import cn.tedu.charging.cost.mapper.CostRuleMapper;
import cn.tedu.charging.cost.service.CostService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Service
public class CostServiceImpl implements CostService {

    @Override
    public ProgressCostVO calculateCost(ProgressCostParam costParam) {
        //充电度数 * 每度电的价格 = 花费  当前同步进度的花费

        //充电度数是设备同步的
        //每度电的价格是从哪来 ?
        //计价规则的概念
        // 不同的站 不同的时间 不同的设备类型 会导致价格不同
        // 为了管理这些计价规则 创建一个计价规则表
        // charging_cost_rule
        /**
         * 名称,   开始时间 结束时间 价格  枪类型  站点id
         * 尖上午     8     12    1.50   快      5
         * 尖下午,   14     18    1.50   快      5
         * 峰,      18     24    1.40   快      5
         * 平,      12     14    1.30   快      5
         * 谷,      0      8     1.00   快      5
         * 尖上午    8     12     1.80   快      6
         */
        //获取 当前时间 去计价规则表 查询 当前时间 匹配的时间段的 计价规则 时间不同价格不同

        //获取当前时间 小时
        //int hours = new Date().getHours();
        //int newHours = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
        int hour = LocalDateTime.now().getHour();
        //通过时间和场站id 匹配计价规则
        ChargingCostRulePO costRulePO =
                matchCostRuleByStationId(costParam.getStationId(), hour);
        log.debug("当前时间{},匹配的计价规则:{}",hour,costRulePO);
        if (costRulePO == null) {
            log.info("没有匹配的计价规则,应该告警告警,通知管理员");
            //或者使用一个默认的价格 1.4
        }
        //获取匹配的计价规则的 每度电的价格
        BigDecimal powerFee = costRulePO.getPowerFee();
        //计算当前同步的订单的计费信息
        log.debug("当前时间{},同步的充电的度数是{},每度电的价格:{}",hour,costParam.getChargingCapacity(),powerFee);
        BigDecimal onceCost = calculateOnceCost(costParam.getChargingCapacity(), powerFee);
        log.debug("当前时间{},同步的充电的度数是{},每度电的价格:{},花费{}",hour,costParam.getChargingCapacity(),powerFee,onceCost);

        //如何计算总价 总价 每次同步充电度数 计算后的价格 累加起来
        BigDecimal totalCost = getTotalCost(costParam.getOrderNo(), onceCost);
        //计算时长 已经充了多长时间 当前时间 减去 第一次同步的时间
        Long totalTime = getTotalTime(costParam.getOrderNo());

        //定义返回结果
        ProgressCostVO vo = new ProgressCostVO();
        //设置总花费
        vo.setTotalCost(totalCost);
        //设置每度电的价格 如果跨时间段 可以每度电的价格不同
        vo.setPowerFee(powerFee);
        //总时间
        vo.setTotalTime(totalTime);
        //总度数
        //*****************同步策略 不包含 同步的是 实际充电度数********************
        // 简单 考虑数据丢失的影响  总度数 需要参考 价格进行 累加
        //  同步次数  实际充电度数 同步度数   总充电度数
        //    1         1         1         1
        //    2         10       10         11   丢了总度 不是 22 1 + 11 = 12 度 少了 10度
        //    3         11       11         22
        //*****************同步策略 包含(包含了原来同步的度数) 同步的是 实际充电度数 ********************
        //   每次同步的都是 总度数 实际度数 需要 用户当前同步 减去 上次同步的度数
        //    1         1         1         1
        //    2         10        11        11 丢了 下次同步 会保护丢失的10度
        //    3         11        22        22
        // 如果每次同步的的是 实际的充电度数 总度数 和 价格一样 累加计算
        //
        //vo.setTotalCapacity();
        //返回结果
        return vo;
    }

    /**
     * 通过订单号 获取已经同步的时长
     * 当前时间 减去 开始时间
     * @param orderNo
     * @return
     */
    private Long getTotalTime(String orderNo) {
        //定义时间计算的redis的 key 按照 订单号 区分
        String key = "charging_cost_time_" + orderNo;
        //操作字符串类型
        ValueOperations<String, Long>
                valueOperations = redisTemplate.opsForValue();
        //获取当前订单号的开始时间
        Long startTime = valueOperations.get(key);
                //valueOperations.get(key);
        //判断时间是否为空
        if (startTime == null) {
            //说明是第一次 同步
            //记录第一次同步的时间
            startTime = System.currentTimeMillis();
            //保存开始时间
            valueOperations.set(key,startTime);
        }
        //不是第一次 同步
        Long now = System.currentTimeMillis();
        // 用 当前时间 减去 开始时间
        Long costTime = now - startTime;
        return costTime;
    }


    /**
     * 通过订单号 获取花费
     * 如果每没有花费 表示第一次计算价格 把价格保存起来
     * 如果有花费 当前的花费 和 原来的花费 加起来
     * @param orderNo
     * @param onceCost
     * @return
     *127.0.0.1:6379> INCRBY cost_321 10
     * (integer) 10  返回值 (0上次 + 10本次)
     *127.0.0.1:6379> INCRBY cost_321 15
     * (integer) 25  返回值 (10上次 + 15本次)
     *127.0.0.1:6379> INCRBY cost_321 16
     * (integer) 41  返回值 (25上次  + 16本次)
     *
     */
    private BigDecimal getTotalCost(String orderNo,BigDecimal onceCost) {
        //定义花费累加的redis的 key 按照 订单号 区别
        String key = "charging_cost_" + orderNo;
        //操作字符串类型
        ValueOperations<String,Double>
                valueOperations = redisTemplate.opsForValue();
        //INCRBY key increment
        Double increment = valueOperations.increment(key, onceCost.doubleValue());
        //返回
        return new BigDecimal(increment).setScale(2, RoundingMode.HALF_UP);


        /*BigDecimal result = null;
        //1 获取
        result = valueOperations.get(key);
        //2 判断是否为空 第一次肯定为空
        if (result == null) {
            valueOperations.set(key,onceCost);
        }else {
            //3 不为空 不是第一次 需要把原来的和现在的加起来
            result = result.add(onceCost);
            valueOperations.set(key,result);
        }*/
        //return result;
    }

    /**
     *
     * @param chargingCapacity
     * @param powerFee
     * @return
     */
    private BigDecimal calculateOnceCost(BigDecimal chargingCapacity,BigDecimal powerFee){
        //BigDecimal 方便计算 提供了一些方法
        // add 加法 multiply 乘法 divide  除法 subtract 减法
        return powerFee
                .multiply(chargingCapacity)
                .setScale(2, RoundingMode.HALF_UP);
    }

    @Autowired
    private CostRuleMapper  costRuleMapper;


    /**
     * 根据站点id 查询当前站点的计价规则
     * @param stationId 站点id
     * @param hour 当前小时
     * @return
     *
     * //去数据库表查询 计价规则
     *         // 方案 1 直接从数据库查询 匹配的计价规则
     *         // select * from charging_cost_rule
     *         // where start_time <= now()
     *         //  and  end_time > now()
     *         //  and  station_id = id
     *         //  and  gun_type = type
     *         // 方案 2 查询当前站所有的计价规则
     *         //  2.1 select * from charging_cost_rule where station_id = id
     *         //  2.2 循环计价规则列表
     *         //  2.3 判断当前时间是否在计价规则的时间段内
     *         //  2.4 匹配成功 如果在时间段内 获取该计价规则的 价格
     *         // 方案 3 缓存
     *         //  3.1 计价规则 存储到Redis中 缓存预热,旁路缓存 为什么要放到缓存 计价规则要被频繁的查询
     *         //  3.2 从缓存中获取 当前所有站的计价规则
     *         //  3.3 循环计价规则列表
     *         //  3.4 匹配成功 如果在时间段内 获取该计价规则的 价格
     */
    private ChargingCostRulePO matchCostRuleByStationId(Integer stationId, Integer hour) {
        //通过场站id 获取所有计价规则
        long start = System.currentTimeMillis();
        log.debug("查询计价规则,开始时间:{}",start);
        List<ChargingCostRulePO> costRules = getCostRulePOSByHash(stationId);
        log.debug("查询计价规则,耗时:{}",System.currentTimeMillis() - start);
        // 循环计价规则列表
        for (ChargingCostRulePO costRule : costRules) {
            //判断当前时间是否在 计价规则的 时间段 内
            if (hour >= costRule.getStartTime()
                    && hour < costRule.getEndTime()) {
                return costRule;
            }
        }
        return null;
    }

    /**
     * 通过场站id 获取所有计价规则
     *
     * //方案1 sql 不推荐
     *         //QueryWrapper queryWrapper = new QueryWrapper();
     *         //等于 =
     *         //queryWrapper.eq("station_id",stationId);
     *         // start_time 开始时间 8  end_time 结束时间  12  now 当前时间 10
     *
     *         // 8 <= 10 <= 12 错误的
     *
     *         // 8 <= 10 < 12
     *         // 8 <= 8 < 12
     *
     *         // 8 < 10 <= 12
     *         // 8 < 12 <= 12
     *
     *         // queryWrapper lt ge eq le gt 自己了解API
     *
     *
     *  //方案2 查询当前站 stationId 所有的计价规则
     *
     *  //方案3 缓存 计价规则 推荐
     *  // 旁路缓存
     *
     *  // 1 先从缓存中取
     *  // 2 如果缓存中没有数据
     *  // 3 去数据库查
     *  // 4 把数据库的数据 放入缓存
     *
     *  // 缓存预热 参考 附近充电站 预热 充电站的逻辑
     *
     * @param stationId
     * @return
     */
    private List<ChargingCostRulePO> getCostRulePOSByString(Integer stationId) {
        // 先从缓存中获取
        //定义操作redis字符串的对象 redisTemplate.opsForValue()
        ValueOperations<String,List<ChargingCostRulePO>>
                valueOperations = redisTemplate.opsForValue();

        //定义缓存的key  一个站点ID对应一个计价规则列表
        String key = "cost_rule_" + stationId;
        //从缓存中获取场站id 对应的计价规则列表
        log.debug("从缓存中获取:{}",key);
        List<ChargingCostRulePO> cacheCostRules = valueOperations.get(key);
        if (!CollectionUtils.isEmpty(cacheCostRules)) {
            log.debug("缓存命中,缓存中有数据,直接返回");
            return cacheCostRules;
        }else {
            log.debug("缓存未命中,缓存中没有数据,从数据库中获取,并放入缓存");
            //查询当前站 stationId 所有的计价规则
            QueryWrapper queryWrapper = new QueryWrapper();
            queryWrapper.eq("station_id", stationId);
            //计价规则列表
            log.debug("从数据库查询");
            List<ChargingCostRulePO> costRules =
                    costRuleMapper.selectList(queryWrapper);
            //判断数据库的计价规则列表是否为空
            if (!CollectionUtils.isEmpty(costRules)) {
                log.debug("数据库中有数据,保存到缓存,返回");
                //不是空 保存到缓存中
                valueOperations.set(key,costRules);
                //返回
                return costRules;
            }else {
                log.error("数据库中没有该站点:{}的计价规则",stationId);
            }
        }
        return null;
    }


    //@Autowired
    //private RedisTemplate redisTemplate;

    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 为了提升性能 实现局部更新 计价规则采用Redis的Hash类型
     * @param stationId
     * @return
     */
    private List<ChargingCostRulePO> getCostRulePOSByHash(Integer stationId) {
        // 先从缓存中获取
        //定义操作redisHash的对象 redisTemplate.opsForHash()
        //大key 站点id 小key 计价规则的id value 计价规则
        HashOperations<String,String,ChargingCostRulePO> hashOperations
                = redisTemplate.opsForHash();

        log.debug("使用RedisTemplate对象:{}",redisTemplate);
        RedisSerializer keySerializer = redisTemplate.getHashKeySerializer();
        log.debug("keySerializer:{}", keySerializer);



        //定义缓存的key  一个站点ID对应一个计价规则列表
        String key = "cost_rule_hash_" + stationId;
        //从缓存中获取场站id 对应的计价规则列表
        log.debug("从缓存中获取:{}",key);
        //HGETALL
        Map<String, ChargingCostRulePO> entries = hashOperations.entries(key);
        if (!CollectionUtils.isEmpty(entries)) {
            log.debug("缓存命中,缓存中有数据,直接返回");
            //values 获取的是map中所有的value
            Collection<ChargingCostRulePO> values = entries.values();
            //lambda表达式
            List<ChargingCostRulePO> collect = values.stream().collect(Collectors.toList());
            return collect;
        }else {
            log.debug("缓存未命中,缓存中没有数据,从数据库中获取,并放入缓存");
            //查询当前站 stationId 所有的计价规则
            QueryWrapper queryWrapper = new QueryWrapper();
            queryWrapper.eq("station_id", stationId);
            //计价规则列表
            log.debug("从数据库查询");
            List<ChargingCostRulePO> costRules =
                    costRuleMapper.selectList(queryWrapper);
            //判断数据库的计价规则列表是否为空
            if (!CollectionUtils.isEmpty(costRules)) {
                log.debug("数据库中有数据,保存到缓存,返回");
                //不是空 保存到缓存中
                //put 是一个一个保存 void put(H key, HK hashKey, HV value);
                //putAll 批量保存 void putAll(H key, Map<? extends HK, ? extends HV> m);  mysql 如果操作多条推荐用 batchInsert
                //如何把list转换为map?
                // 遍历 得自己会写
//                Map<Integer, ChargingCostRulePO> map = new HashMap<>();
//
//                costRules.forEach(costRule -> map.put(costRule.getId(),costRule));
//
//                for (ChargingCostRulePO costRule : costRules) {
//                    map.put(costRule.getId(),costRule);
//                }
                // 通过 lambda list 转换为 map
                Map<String, ChargingCostRulePO> collect = costRules.stream().
                        collect(Collectors.toMap(k -> String.valueOf(k.getId()), v -> v));
                hashOperations.putAll(key,collect);
                //返回
                return costRules;
            }else {
                log.error("数据库中没有该站点:{}的计价规则",stationId);
            }
        }
        return null;
    }
}
